﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Xml;

namespace Geotagger
{
    [CLSCompliant(true)]
    public class Geotagger
    {
        #region private variables

        private const int EXIF_GPS_NMEA_VERSION = 0x0;
        private const int EXIF_GPS_LATITUDE_ORIENT = 0x1;
        private const int EXIF_GPS_LATITUDE = 0x2;
        private const int EXIF_GPS_LONGITUDE_ORIENT = 0x3;
        private const int EXIF_GPS_LONGITUDE = 0x4;
        private const int EXIF_GPS_ALTITUDE_REF = 0x5;
        private const int EXIF_GPS_ALTITUDE = 0x6;
        private const int EXIF_GPS_TIME_STAMP = 0x7;
        private const int EXIF_GPS_SATELLITES = 0x8;
        private const int EXIF_GPS_DATE_STAMP = 0x1D;

        private const int EXIF_TYPE_RATIONAL = 0x5;
        private const int EXIF_TYPE_BYTE = 0x1;

        private string _tcxPath = "";

        private string _imageFolder = "";

        //private DateTime _gpsDateTime = DateTime.MinValue;
        private SortedDictionary<DateTime, GeoDataPoint> _data = new SortedDictionary<DateTime, GeoDataPoint>();
        #endregion
        #region Accessors


        public string ImageFolder
        {
            get
            {
                return _imageFolder;
            }
            set { _imageFolder = value; }
        }




        public string TcxPath
        {
            get { return _tcxPath; }
            set
            {
                if (File.Exists(value))
                {
                    _tcxPath = value;
                    loadXML();
                }
            }
        }
        #endregion
        #region Private Functions

        private static void SetGeoTagDataOnImage(EXIF.EXIFextractor exifData, GeoDataPoint locationInformation)
        {
          byte[] longbytes = GetLatLongBytes(locationInformation.Longitude);
          exifData.setTag(EXIF_GPS_LONGITUDE_ORIENT, EastOrWest(locationInformation.Longitude));
          exifData.setTag(EXIF_GPS_LONGITUDE, longbytes.Length, EXIF_TYPE_RATIONAL, longbytes);

          byte[] latbytes = GetLatLongBytes(locationInformation.Latitude);
          exifData.setTag(EXIF_GPS_LATITUDE, latbytes.Length, EXIF_TYPE_RATIONAL, latbytes);
          exifData.setTag(EXIF_GPS_LATITUDE_ORIENT, NorthOrSouth(locationInformation.Latitude));

          byte[] altitudebytes = GetAltitudeBytes(locationInformation.Altitude);
          exifData.setTag(EXIF_GPS_ALTITUDE_REF, 1, EXIF_TYPE_BYTE, BitConverter.GetBytes(0));
          exifData.setTag(EXIF_GPS_ALTITUDE, altitudebytes.Length, EXIF_TYPE_RATIONAL, altitudebytes);
        }

        
        private void loadXML()
        {

            Debug.Assert(File.Exists(_tcxPath));

            XmlDocument x = new XmlDocument();
            try
            {
                x.Load(_tcxPath);
            }
            catch
            {
                //make this better later
                throw;
            }

            XmlNodeList nodes = x.GetElementsByTagName("Trackpoint");

            foreach (XmlNode n in nodes)
            {

                try
                {

                    string latitude = n.ChildNodes.Item(1).ChildNodes.Item(0).InnerText;
                    string longitude = n.ChildNodes.Item(1).ChildNodes.Item(1).InnerText;
                    string rawTime = n.ChildNodes.Item(0).InnerText;
                    string altitude = n.ChildNodes.Item(2).InnerText;
                    DateTime time = DateTime.MinValue;
                    DateTime.TryParse(rawTime, out time);
                    if (time != DateTime.MinValue)
                    {
                        _data.Add(time, new GeoDataPoint(time, latitude, longitude, altitude));
                    }
                    else
                    {
                        Trace.WriteLine("invalid time " + rawTime);
                    }

                }
                catch
                {
                    Trace.WriteLine("only losers swallow exceptions in loadXML");
                }

            }

        }
        private static string NorthOrSouth(string latitude)
        {
            float temp;
            float.TryParse(latitude, out temp);
            return (temp > 0 ? "N" : "S");
        }
        private static string EastOrWest(string longitude)
        {
            float temp;
            float.TryParse(longitude, out temp);
            return (temp > 0 ? "E" : "W");
        }
        private static Byte[] GetAltitudeBytes(string input)
        {

            Byte[] outputBytes = new Byte[8];
            Byte[] work;

            outputBytes.Initialize();

            Single altitude = Math.Abs(Convert.ToSingle(input));
            altitude = (float)Math.Round(altitude, 2);
            altitude *= 100;

            outputBytes.Initialize();

            work = BitConverter.GetBytes((UInt32)altitude);
            work.CopyTo(outputBytes, 0);
            outputBytes[4] = 0x64;

            return outputBytes;

        }
        private static Byte[] GetLatLongBytes(string input)
        {

            Byte[] outputBytes = new Byte[24];
            Byte[] work;
            int degrees, minutes, seconds;
            Single coordinates = Math.Abs(Convert.ToSingle(input));

            outputBytes.Initialize();

            degrees = (int)Math.Truncate(coordinates);
            seconds = (int)(3600 * (coordinates - degrees));
            minutes = (int)Math.Truncate((float)(seconds / 60));
            seconds = seconds - 60 * minutes;

            work = BitConverter.GetBytes(degrees);
            work.CopyTo(outputBytes, 0);
            outputBytes[4] = 1;

            work = BitConverter.GetBytes(minutes);
            work.CopyTo(outputBytes, 8);
            outputBytes[12] = 0x1;


            work = BitConverter.GetBytes(seconds);
            work.CopyTo(outputBytes, 16);
            outputBytes[20] = 0x1;

            return outputBytes;

        }

        #endregion
        #region Public methods

        public static void SetGeocodeDataManually(string fileName, GeoDataPoint gdp) {

          if (!File.Exists(fileName))
            throw new FileNotFoundException();

          if (!(fileName.ToUpper().EndsWith("JPG")))
            throw new ArgumentException("only JPG is supported");

          Trace.WriteLine("Setting GPS Info on " + fileName);
          
          Bitmap b = (Bitmap) Image.FromFile(fileName);

          EXIF.EXIFextractor e = new EXIF.EXIFextractor(ref b, "");

          SetGeoTagDataOnImage(e, gdp);

        }

        public void SetGeocodeData()
        {


            Debug.Assert(_data != null);
            Debug.Assert(Directory.Exists(_imageFolder));

            DirectoryInfo di = new DirectoryInfo(_imageFolder);

            foreach (FileInfo fi in di.GetFiles("*.jpg"))
            {

                Trace.WriteLine("Processing " + fi.FullName);

                System.Drawing.Bitmap bi = (Bitmap)Bitmap.FromFile(fi.FullName);

                EXIF.EXIFextractor er = new EXIF.EXIFextractor(ref bi, "", "");

                string tempString = (string)er["DTOrig"];

                DateTime photoDateTime = DateTime.MinValue;


                if (!String.IsNullOrEmpty(tempString))
                {

                  string fixedupString = tempString.Substring(0, 4) + "-" +
                      tempString.Substring(5, 2) + "-" +
                      tempString.Substring(8, 3) +
                      tempString.Substring(11, 8);

                  //"2008:01:31 10:39:01\0"

                  if (!DateTime.TryParse(fixedupString, out photoDateTime))
                    throw new ArgumentException(fixedupString + " cant parse into a datetime");

                }

                GeoDataPoint keeper = null;
                foreach (KeyValuePair<DateTime, GeoDataPoint> kvp in _data)
                {

                    GeoDataPoint g = (GeoDataPoint)kvp.Value;
                    if (keeper == null) keeper = g;
                    //Trace.WriteLine("testing photoDateTime " + photoDateTime.ToString() + " against " + g.Date.ToString());

                    if (photoDateTime < g.Date)
                    {
                        Trace.WriteLine("photoDateTime is less than the GPS date");
                        break;
                    }
                    keeper = g;

                }

                Debug.Assert(photoDateTime > DateTime.MinValue);
                Debug.Assert(keeper.Date > DateTime.MinValue);

                TimeSpan s = photoDateTime - keeper.Date;

                if (s.Duration() <= TimeSpan.FromHours(1))
                {

                    Trace.WriteLine("putting gps data on " + fi.Name);

                    SetGeoTagDataOnImage(er, keeper);

                    string newPath = Path.GetDirectoryName(fi.FullName) + "\\GeoTagged";
                    if (!Directory.Exists(newPath))
                        Directory.CreateDirectory(newPath);

                    string newFile = newPath + "\\" + Path.GetFileName(fi.Name);

                    bi.Save(newFile);
                }
                else
                {
                    Trace.WriteLine("Can't find good timestamp.  Closest is");
                    Trace.WriteLine("photoDateTime = " + photoDateTime.ToString());
                    Trace.WriteLine("keeper.Date = " + keeper.Date.ToString());
                }
            }

        }


        public void DumpGPSInfo(string fileName)
        {
            if (File.Exists(fileName))
            {

                EXIF.EXIFextractor e = new EXIF.EXIFextractor(fileName, "", "");
                Trace.WriteLine("LongitudeRef " + e["Gps LongitudeRef"].ToString());
                Trace.WriteLine("Longitude " + e["Gps Longitude"].ToString());
                Trace.WriteLine("LatitudeRef " + e["Gps LatitudeRef"].ToString());
                Trace.WriteLine("Latitude " + e["Gps Latitude"].ToString());
                Trace.WriteLine("AltitudeRef " + e["Gps AltitudeRef"].ToString());
                Trace.WriteLine("Altitude " + e["Gps Altitude"].ToString());

            }

        }

        #endregion

    }
    public class GeoDataPoint
    {
        #region Private Variables
        private DateTime _date = DateTime.Now;
        private string _latitude = INVALID_LATITUDE_LONGITUDE;
        private string _longitude = INVALID_LATITUDE_LONGITUDE;
        private string _altitude = INVALID_ALTITUDE;
        #endregion
        #region Public Variables
        public const string INVALID_LATITUDE_LONGITUDE  = "999999999";
        public const string INVALID_ALTITUDE  = "999999999";
        #endregion
        #region Constructors
        public GeoDataPoint(DateTime date, string latitude, string longitude, string altitude)
        {
          
            float temp;
            _date = date;

            if (float.TryParse(latitude, out temp))
              _latitude = latitude;


            if (float.TryParse(longitude, out temp))
              _longitude = longitude;

            if (float.TryParse(altitude, out temp))
              _altitude = altitude;
        }
        public GeoDataPoint(string latitude, string longitude, string altitude) { 
        
          /*
           <Trackpoint>
            <Time>2006-06-14T09:21:55Z</Time>
            <Position>
              <LatitudeDegrees>35.1468161</LatitudeDegrees>
              <LongitudeDegrees>-80.7064572</LongitudeDegrees>
            </Position>
            <AltitudeMeters>221.7211914</AltitudeMeters>
          </Trackpoint>
          */

          _date = DateTime.Now;

          float temp;

          if (float.TryParse(latitude, out temp))
            _latitude = latitude;

          if (float.TryParse(longitude, out temp))
            _longitude = longitude;

          if (float.TryParse(altitude, out temp))
            _altitude = altitude;

        }
        #endregion
        #region Accessors
        public DateTime Date { get { return _date; } }
        public string Latitude { get { return _latitude; } }
        public string Longitude { get { return _longitude; } }
        public string Altitude { get { return _altitude; } }
        #endregion
    }
}
